﻿using JwtConstants = MicroCloud.JwtConstants;

namespace MicroCloud.Authentication.JwtBearer
{
    #region "JwtBearer服务实现"
    /// <summary>
    /// JwtBearer服务实现
    /// </summary>
    /// <typeparam name="TUser">用户实体类型</typeparam>
    /// <typeparam name="TUserKey">用户实体主键类型</typeparam>
    public class JwtBearerService<TUser, TUserKey> : IJwtBearerService
        where TUser : UserBase<TUserKey>
        where TUserKey : struct, IEquatable<TUserKey>
    {
        //字段
        private readonly JwtOption _jwtOptions;
        private readonly IServiceProvider _provider;
        private readonly JwtSecurityTokenHandler _tokenHandler = new();

        //属性
        private IDistributedCache Cache => _provider.GetService<IDistributedCache>();

        #region "构造函数"
        #region "初始化一个JwtBearer服务实现的新实例"
        /// <summary>
        /// 初始化一个JwtBearer服务实现 <see cref="JwtBearerService{TUser, TUserKey}"/> 的新实例
        /// </summary>
        /// <param name="provider"></param>
        public JwtBearerService(IServiceProvider provider)
        {
            _provider = provider;
            _jwtOptions = _provider.GetMicroCloudOptions().Jwt;
        }
        #endregion

        #endregion

        #region "方法"
        #region "创建指定用户的JwtToken信息"
        /// <summary>
        /// 创建指定用户的JwtToken信息
        /// </summary>
        /// <param name="userId">用户编号</param>
        /// <param name="organizationId">组织机构编号</param>
        /// <param name="clientId">客户端ID</param>
        /// <param name="clientType">客户端类型</param>
        /// <returns>JwtToken信息</returns>
        public async Task<JsonWebToken> CreateToken(string userId, string organizationId, string clientId, RequestClientType clientType = RequestClientType.Browser)
        {
            return await CreateToken(userId, organizationId, clientId, clientType, null);
        }
        #endregion
        #region "使用RefreshToken获取新的JwtToken信息"
        /// <summary>
        /// 使用RefreshToken获取新的JwtToken信息
        /// </summary>
        /// <param name="refreshToken">刷新Token字符串</param>
        /// <returns>JwtToken信息</returns>
        public virtual async Task<JsonWebToken> RefreshToken(string refreshToken)
        {
            Check.NotNull(refreshToken, nameof(refreshToken));

            JwtSecurityToken jwtSecurityToken = _tokenHandler.ReadJwtToken(refreshToken);
            var audience = jwtSecurityToken.Audiences.FirstOrDefault();
            TokenValidationParameters parameters = new()
            {
                ValidIssuer = _jwtOptions.Issuer ?? JwtConstants.DefaultIssuer,
                // ValidAudience = _jwtOptions.Audience ?? JwtConstants.DefaultAudience,
                ValidAudience = audience,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.Secret))
            };

            string clientId = jwtSecurityToken.Claims.FirstOrDefault(m => m.Type == ClaimTypeConstants.ClientId)?.Value;
            if (clientId == null)
            {
                throw new Exception(I18N.T("RefreshToken 中不包含 {0} 声明", "ClientId"));
            }
            // 修复 RefreshToken 处理的BUG，这是一个坑，赋值的时候用的 ClaimTypes.NameIdentifier ，取数时只能通过字符串 "nameid" 来，不知道什么原因。
            string userId = jwtSecurityToken.Claims.FirstOrDefault(m => m.Type == ClaimTypes.NameIdentifier)?.Value;
            userId ??= jwtSecurityToken.Claims.FirstOrDefault(m => m.Type == ClaimTypeConstants.NameId)?.Value;
            if (userId == null)
            {
                throw new Exception(I18N.T("RefreshToken 中不包含 {0} 声明", "UserId"));
            }

            RequestClientType? clientType = jwtSecurityToken.Claims.FirstOrDefault(m => m.Type == ClaimTypeConstants.ClientType)?.Value.CastTo<RequestClientType>()
                ?? RequestClientType.Browser;

            UserManager<TUser> userManager = _provider.GetService<UserManager<TUser>>();
            RefreshToken existRefreshToken = await userManager.GetRefreshTokenAsync(userId, clientId);
            if (existRefreshToken == null /*|| existRefreshToken.Value != refreshToken*/)
            {
                throw new Exception(I18N.T("RefreshToken 不存在"));
            }
            else if (existRefreshToken.ExpirationTimeUtc <= DateTime.UtcNow)
            {
                //删除指定ClientId过期的RefreshToken
                IUnitOfWork unitOfWork = _provider.GetUnitOfWork(true);
                userManager = _provider.GetRequiredService<UserManager<TUser>>();
                var result = await userManager.RemoveRefreshTokenAsync(userId, clientId);
                if (result.Succeeded)
                {
                    await unitOfWork.CommitAsync();
                }
                throw new Exception(I18N.T("RefreshToken 已过期"));
            }


            var principal = _tokenHandler.ValidateToken(refreshToken, parameters, out _);
            string currentOrganizationId = principal.Claims.FirstOrDefault(m => m.Type == ClaimTypeConstants.CurrentOrganizationId)?.Value;
            string currentDepartmentId = principal.Claims.FirstOrDefault(m => m.Type == ClaimTypeConstants.CurrentDepartmentId)?.Value;
            string organizationId = currentDepartmentId.IsMissing() ? currentOrganizationId : currentDepartmentId;

            JsonWebToken token = await CreateToken(userId, organizationId, existRefreshToken.ClientId, clientType.Value, existRefreshToken);

            return token;
        }
        #endregion

        #region "移除订阅者Token缓存"
        /// <summary>
        /// 移除订阅者Token缓存
        /// </summary>
        /// <param name="userId">用户编号</param>
        /// <param name="clientId">客户端ID</param>
        /// <returns></returns>
        public async Task RemoveAudienceCache(string userId, string clientId)
        {
            var audience = GetAudience(userId, clientId);
            //移除缓存
            await Cache.RemoveAsync(audience);
        }
        #endregion

        #endregion

        #region "私有方法"
        #region "创建Token"
        /// <summary>
        /// 创建Token
        /// </summary>
        /// <param name="userId">用户编号</param>
        /// <param name="organizationId">组织机构编号</param>
        /// <param name="clientId">客户端ID</param>
        /// <param name="clientType">客户端类型</param>
        /// <param name="refreshToken">refreshToken信息</param>
        /// <returns></returns>
        private async Task<JsonWebToken> CreateToken(string userId, string organizationId, string clientId, RequestClientType clientType, RefreshToken refreshToken)
        {
            Check.NotNullOrEmpty(userId, nameof(userId));

            clientId = clientId.IsMissing() ? Guid.NewGuid().ToString("N").ToUpper() : clientId;

            var userClaimsProvider = _provider.GetRequiredService<IUserClaimsProvider>();
            //组织声明处理
            var claims = await userClaimsProvider.CreateClaims(userId);
            claims = await userClaimsProvider.HandleCurrentOrganizationClaims(claims, organizationId);

            //客户端声明信息处理
            List<Claim> claimList = claims.ToList();
            claimList.Add(new Claim(ClaimTypeConstants.ClientId, clientId));
            claimList.Add(new Claim(ClaimTypeConstants.ClientType, clientType.ToString()));

            // New RefreshToken
            var (token, expires) = CreateToken(claimList, _jwtOptions, JwtTokenType.RefreshToken, refreshToken);
            string refreshTokenStr = token;
            DateTime refreshTokenExpires = expires;
            IUnitOfWork unitOfWork = _provider.GetUnitOfWork(true);
            UserManager<TUser> userManager = _provider.GetService<UserManager<TUser>>();
            refreshToken = new RefreshToken() { ClientId = clientId, Value = refreshTokenStr, ExpirationTimeUtc = refreshTokenExpires };
            IdentityResult result = await userManager.SetRefreshTokenAsync(userId, refreshToken);
            if (result.Succeeded)
            {
                await unitOfWork.CommitAsync();
                IEventBus eventBus = _provider.GetRequiredService<IEventBus>();
                OnlineUserCacheRemoveEventData eventData = new() { UserIds = new[] { userId } };
                await eventBus?.PublishAsync(eventData);
            }

            // New AccessToken
            (token, expires) = CreateToken(claimList, _jwtOptions, JwtTokenType.AccessToken);
            string accessTokenStr = token;
            DateTime accessTokenExpires = expires;

            return new JsonWebToken()
            {
                AccessToken = accessTokenStr,
                AccessTokenExpiresUtc = accessTokenExpires.ToJsGetTime().CastTo<long>(0),
                RefreshToken = refreshTokenStr,
                RefreshTokenExpiresUtc = refreshTokenExpires.ToJsGetTime().CastTo<long>(0)
            };
        }
        #endregion
        #region "创建Token"
        /// <summary>
        /// 创建Token
        /// </summary>
        /// <param name="claims">声明集合</param>
        /// <param name="options">Jwt配置选择</param>
        /// <param name="tokenType">Token类型</param>
        /// <param name="refreshToken">刷新Token</param>
        /// <returns></returns>
        private (string, DateTime) CreateToken(IEnumerable<Claim> claims, JwtOption options, JwtTokenType tokenType, RefreshToken refreshToken = null)
        {
            string secret = options.Secret;
            if (secret.IsMissing())
            {
                throw new Exception(I18N.T("创建 JwtToken 时 Secret 为空，请在 MicroCloud:Jwt:Secret 节点中进行配置"));
            }

            string userId = claims.FirstOrDefault(m => m.Type == ClaimTypes.NameIdentifier)?.Value;
            string clientId = claims.FirstOrDefault(m => m.Type == ClaimTypeConstants.ClientId)?.Value;

            var audience = GetAudience(userId, clientId);

            DateTime expires;
            DateTime now = DateTime.UtcNow;
            if (tokenType == JwtTokenType.AccessToken)
            {
                if (refreshToken != null)
                {
                    throw new Exception(I18N.T("创建 AccessToken 时不需要 RefreshToken"));
                }

                double minutes = (options.AccessExpireMins > 0 ? options.AccessExpireMins : JwtConstants.DefaultAccessExpireMins).ToDouble(); //默认5分钟
                expires = now.AddMinutes(minutes);
                //设置Jwt订阅者缓存
                RefreshAudienceTokenCache(audience, userId, clientId, minutes);
            }
            else
            {
                if (refreshToken == null || !options.IsRefreshAbsoluteExpired)
                {
                    double minutes = (options.RefreshExpireMins > 0 ? options.RefreshExpireMins : JwtConstants.DefaultRefreshExpireMins).ToDouble(); // 默认7天
                    expires = now.AddMinutes(minutes);
                    //设置Jwt订阅者缓存
                    RefreshAudienceTokenCache(audience, userId, clientId, minutes);
                }
                else
                {
                    expires = refreshToken.ExpirationTimeUtc;
                }
            }
            SecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
            SigningCredentials credentials = new(key, SecurityAlgorithms.HmacSha256Signature);

            SecurityTokenDescriptor descriptor = new()
            {
                Subject = new ClaimsIdentity(claims),
                Issuer = options.Issuer,
                Audience = audience,
                SigningCredentials = credentials,
                NotBefore = now,
                IssuedAt = now,
                Expires = expires
            };
            SecurityToken token = _tokenHandler.CreateToken(descriptor);
            string accessToken = _tokenHandler.WriteToken(token);

            return (accessToken, expires);
        }
        #endregion

        #region "获取订阅者键值"
        /// <summary>
        /// 获取订阅者键值
        /// </summary>
        /// <param name="userId">用户编号</param>
        /// <param name="clientId">客户端ID</param>
        /// <returns></returns>
        private static string GetAudience(string userId, string clientId)
        {
            Check.NotNullOrEmpty(userId, nameof(userId));
            Check.NotNullOrEmpty(clientId, nameof(clientId));

            var audience = $"{userId}{SettingKeyConstants.Separator}{clientId}";

            return audience;
        }
        #endregion

        #region "刷新订阅者Token缓存"
        /// <summary>
        /// 刷新订阅者Token缓存
        /// </summary>
        /// <param name="audience">订阅者键</param>
        /// <param name="userId">用户编号</param>
        /// <param name="clientId">客户端ID</param>
        /// <param name="expiresMinutes">过期时间（分钟）</param>
        /// <returns></returns>
        private void RefreshAudienceTokenCache(string audience, string userId, string clientId, double expiresMinutes)
        {
            var result = $"{userId}{SettingKeyConstants.Separator}{clientId}{SettingKeyConstants.Separator}{expiresMinutes}";
            //移除相同用户ID和客户端ID的所有缓存
            //Cache.Remove(audience);
            //设置Jwt订阅者缓存
            DistributedCacheEntryOptions options = new();
            options.SetAbsoluteExpiration(TimeSpan.FromMinutes(expiresMinutes));
            Cache.Set(audience, result, options);
        }
        #endregion

        #endregion

        #region "私有枚举"
        #region "Jwt令牌类型"
        /// <summary>
        /// Jwt令牌类型
        /// </summary>
        private enum JwtTokenType
        {
            /// <summary>
            /// 访问令牌
            /// </summary>
            AccessToken = 1,
            /// <summary>
            /// 刷新令牌
            /// </summary>
            RefreshToken = 2,
        }
        #endregion

        #endregion

    }
    #endregion

}
